/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is Forte for Java, Community Edition. The Initial
* Developer of the Original Code is Sun Microsystems, Inc. Portions
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.modules.emacs;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;
// XXX
import org.openide.util.Utilities;
public class Connection implements Runnable, Protocol {
static boolean DEBUG = false;
private static final String THREAD_MAGIC = "org.netbeans.modules.emacs.Connection.THREAD_MAGIC:";
static final int KILL_PORT = 9166;
private static final Object ERROR_RETURN = new Object () {
public String toString () {
return "<Error>";
}
};
// This section implements Connection as a server.
/** List of waiting servers by port. These listen for connections from remote targets.
* @associates Integer*/
private static Set servers; // Set<int port>
/** Get a list of all server ports currently running. */
public synchronized static Set getServerPorts () {
if (servers == null)
return Collections.EMPTY_SET;
else
return new HashSet (servers);
}
/** Start a new server on a port (if it was not started already). */
public synchronized static void startServer (final int port) {
if (servers == null) servers = Collections.synchronizedSet (new HashSet ());
if (! servers.contains (new Integer (port))) {
try {
if (DEBUG) System.err.println ("Starting a Connection server on port " + port + " (I am " + InetAddress.getLocalHost () + ")");
} catch (UnknownHostException uhe) {
uhe.printStackTrace ();
}
Thread t = new Thread (new Runnable () {
public void run () {
ServerSocket ssock;
try {
try {
ssock = new ServerSocket (port);
} catch (BindException be) {
if (DEBUG) {
System.err.println("BindException caught, will retry after 10 sec...");
be.printStackTrace ();
}
// First try to kill off another instance of this class if there is one:
InetAddress localhost = InetAddress.getLocalHost ();
try {
Socket s = new Socket (localhost, port, localhost, KILL_PORT);
s.close ();
} catch (IOException ioe) {
// Fine, ignore.
}
try {
Thread.sleep (10000);
} catch (InterruptedException ie) {
ie.printStackTrace ();
}
ssock = new ServerSocket (port);
}
} catch (IOException e1) {
e1.printStackTrace ();
if (DEBUG) System.err.println("Could not bind server, will stop it");
servers.remove (new Integer (port));
return;
}
while (servers.contains (new Integer (port))) {
try {
if (DEBUG) System.err.println ("Listening for passive connections...");
Socket sock = ssock.accept ();
if (sock.getPort () == KILL_PORT) {
if (DEBUG) System.err.println("Got a KILL_PORT, will stop server");
servers.remove (new Integer (port));
return;
}
ConnectionInfo info = new ConnectionInfo (sock.getInetAddress (), 0);
synchronized (Connection.class) {
if (connections.get (info) != null) {
if (DEBUG) System.err.println ("Error! Duplicate server connection from " + info);
} else {
Connection nue = new Connection (info, sock);
connections.put (info, nue);
if (DEBUG) System.err.println ("Added passive connection: " + nue);
if (DEBUG) System.err.println ("Connections: " + connections.keySet ());
Connection.class.notifyAll ();
}
}
// Now continue listening for connections.
} catch (IOException e2) {
e2.printStackTrace ();
}
}
if (DEBUG) System.err.println ("Shutting down Connection server on port " + port);
}
});
servers.add (new Integer (port));
t.setDaemon (true);
t.start ();
}
}
/** Stop the server on a port (if it was running). */
public synchronized static void stopServer (int port) {
if (servers.contains (new Integer (port))) {
if (DEBUG) System.err.println ("Will shut down Connection server on port " + port);
try {
InetAddress localhost = InetAddress.getLocalHost ();
Socket s = new Socket (localhost, port, localhost, KILL_PORT);
s.close ();
} catch (IOException ioe) {
ioe.printStackTrace ();
}
}
}
/**
* @associates Connection
*/
private final static Map connections = new HashMap (); // Map<ConnectionInfo, Connection>
private static final class ConnectionInfo {
private final InetAddress host;
private final int port;
ConnectionInfo (InetAddress host, int port) {
this.host = host;
this.port = port;
}
public int hashCode () {
return host.hashCode () ^ port;
}
public boolean equals (Object o) {
if (o != null && o instanceof ConnectionInfo) {
ConnectionInfo oo = (ConnectionInfo) o;
return host.equals (oo.host) && port == oo.port;
} else {
return false;
}
}
public String toString () {
return host + ":" + port;
}
}
private final ConnectionInfo info;
private int users;
private final BufferedReader rd;
private final Writer wr;
private final Socket sock;
private boolean connected;
private final boolean autoDisconnect;
private String auth;
private Connection (ConnectionInfo info, String auth) throws IOException {
if (DEBUG) System.err.println ("Creating active connection...");
this.info = info;
this.auth = auth;
sock = new Socket (info.host, info.port);
rd = new BufferedReader (new InputStreamReader (sock.getInputStream ()));
wr = new OutputStreamWriter (sock.getOutputStream ());
users = 1;
connected = true;
autoDisconnect = true; // XXX make configurable in call to addClient ()
wr.write (META_AUTH + " " + auth + "\n");
wr.flush ();
String response = rd.readLine ();
if (response == null)
throw new IOException ("Server closed connection w/o responding to auth");
if (response.equals (META_ACCEPT)) {
if (DEBUG) System.err.println ("Auth OK");
} else if (response.equals (META_REJECT)) {
throw new IOException ("Server rejected auth");
} else {
throw new IOException ("Auth response not understood: " + response);
}
Thread t = new Thread (this);
t.setDaemon (true);
t.start ();
}
private Connection (ConnectionInfo info, Socket sock) throws IOException {
if (DEBUG) System.err.println ("Creating passive connection...");
this.info = info;
this.sock = sock;
rd = new BufferedReader (new InputStreamReader (sock.getInputStream ()));
wr = new OutputStreamWriter (sock.getOutputStream ());
// Wait for them to send desired auth info.
String hello = rd.readLine ();
if (hello == null) throw new IOException ("Attached client closed connection w/o sending auth");
if (! hello.startsWith (META_AUTH + " ")) throw new IOException ("Client sent invalid auth introduction: " + hello);
auth = hello.substring ((META_AUTH + " ").length ());
if (DEBUG) System.err.println ("Got passive auth " + auth);
users = 0;
connected = true;
autoDisconnect = false;
Thread t = new Thread (this);
t.setDaemon (true);
t.setName (THREAD_MAGIC + this);
t.start ();
}
private synchronized void disconnect () throws IOException {
synchronized (Connection.class) {
if (connected) {
if (DEBUG) System.err.println("Disconnecting " + this);
connections.remove (info);
connected = false;
wr.write (META_DISCONNECT + "\n");
wr.close ();
rd.close ();
sock.close ();
}
}
}
protected void finalize () throws Exception {
disconnect ();
}
public static synchronized void disconnectAll () {
if (DEBUG) System.err.println ("Connection.disconnectAll");
Iterator it = new HashSet (connections.values ()).iterator ();
while (it.hasNext ()) {
try {
((Connection) it.next ()).disconnect ();
} catch (IOException ioe) {
ioe.printStackTrace ();
}
}
}
private boolean confirmAuth (String auth) {
return this.auth.equals (auth);
}
public static synchronized Connection addClient (final String host, int port, final String auth) throws IOException {
ConnectionInfo info = new ConnectionInfo (InetAddress.getByName (host), port);
if (DEBUG) System.err.println("Adding Connection client for " + info);
Connection existing = (Connection) connections.get (info);
if (DEBUG) System.err.println ("Connections: " + connections.keySet ());
if (existing != null) {
if (! existing.confirmAuth (auth)) throw new IOException ("Existing connection " + existing + " did not accept auth " + auth);
if (DEBUG) System.err.println("Existing client, " + existing.users + " users");
existing.users++;
return existing;
} else {
if (port == 0) {
// Add passive client (wait for it)
try {
// XXX should avoid showing if there already is one
// XXX should auto-close when a connection is made
SwingUtilities.invokeLater (new Runnable () {
public void run () {
if (DEBUG) System.err.println("addClient II (start)");
String localhost = "localhost";
try {
localhost = InetAddress.getLocalHost ().toString ();
} catch (IOException ioe) {
ioe.printStackTrace ();
}
JOptionPane.showMessageDialog (null, new String[] {
"Please connect from " + host + " to " + localhost + " on port(s) " + getServerPorts () + " with password " + auth,
"within the next sixty seconds, and then close this dialog.",
"To cancel, just close this dialog immediately."
});
if (DEBUG) System.err.println("addClient II (mid)");
synchronized (Connection.class) {
Connection.class.notifyAll ();
}
if (DEBUG) System.err.println("addClient II (end)");
}
});
if (DEBUG) System.err.println("Will wait for a passive connection...");
Connection.class.wait (60000);
if (connections.get (info) != null)
return addClient (host, port, auth);
else
throw new IOException ("Timed out on adding passive connection (or refused to add one)");
} catch (InterruptedException ie) {
ie.printStackTrace ();
throw new IOException (ie.toString ());
}
} else {
// Add active client
if (DEBUG) System.err.println("New client");
Connection nue = new Connection (info, auth);
connections.put (info, nue);
return nue;
}
}
}
public synchronized void removeClient () throws IOException {
if (DEBUG) System.err.println("Removing client " + this + ", " + users + " users");
synchronized (Connection.class) {
if (--users == 0 && autoDisconnect)
disconnect ();
}
}
// read from input stream
public void run () {
String line;
//int padding = 0;
try {
while ((line = rd.readLine ()) != null) {
/*
if (line.equals ("")) {
padding++;
continue;
} else if (padding > 0) {
if (DEBUG) System.err.println ("Proxier got padding: " + padding);
padding = 0;
}
*/
if (DEBUG) System.err.println("Proxier got input line `" + line + "'");
int idx = line.indexOf (' ');
if (idx == -1) {
maybeCallback (line, new Object[0]);
} else {
String type = line.substring (0, idx);
List ls = new ArrayList ();
while (idx != line.length ()) {
if (line.charAt (idx) != ' ')
throw new RuntimeException ("Unexpected char at " + idx + " in `" + line + "': `" + line.charAt (idx) + "'");
idx++;
Object arg;
if (line.charAt (idx) == '"') {
// parse escaped string
StringBuffer text = new StringBuffer ();
idx++; // skip first "
char c;
while ((c = line.charAt (idx)) != '"') {
if (c == '\\') {
char nextChar = line.charAt (++idx);
if (nextChar == 'n')
text.append ('\n');
else if (nextChar == 'r')
text.append ('\r');
else
text.append (nextChar);
} else {
text.append (c);
}
idx++;
}
idx++; // skip last "
arg = text.toString ();
} else if (line.charAt (idx) == 'T') {
arg = Boolean.TRUE;
idx++;
} else if (line.charAt (idx) == 'F') {
arg = Boolean.FALSE;
idx++;
} else if (line.charAt (idx) == '!') {
arg = ERROR_RETURN;
idx++;
} else {
// parse nonnegative integer
StringBuffer digits = new StringBuffer ();
char c;
while (idx != line.length () && Character.isDigit (c = line.charAt (idx))) {
digits.append (c);
idx++;
}
int val;
try {
val = Integer.parseInt (digits.toString ());
} catch (NumberFormatException e) {
e.printStackTrace ();
val = 0;
}
arg = new Integer (val);
}
ls.add (arg);
}
maybeCallback (type, ls.toArray (new Object[ls.size ()]));
}
}
if (connected) {
if (DEBUG) System.err.println ("Connection closed by server");
disconnect ();
}
} catch (IOException e) {
if (connected || DEBUG)
e.printStackTrace ();
if (! connected && DEBUG)
System.err.println("...but that is to be expected.");
}
}
/**
* @associates Object
*/
private Map holds = Collections.synchronizedMap (new HashMap ());
private void maybeCallback (final String type, final Object[] args) {
try {
int seq = Integer.parseInt (type);
Integer seqI = new Integer (seq);
Object hold = holds.get (seqI);
if (hold == null) {
System.err.println ("dead sequence: " + seq); // XXX
} else {
//if (DEBUG) System.err.println ("holds.put real result for seqnum " + seq);
holds.put (seqI, args);
synchronized (hold) {
// XXX does notify() suffice??
hold.notifyAll ();
}
}
} catch (NumberFormatException ign) {
// OK, normal string callback
SwingUtilities.invokeLater (new Runnable () {
public void run () {
callback (type, args);
}
});
}
}
/**
* @associates EmacsListener
*/
private Set listeners = new HashSet (); // Set<EmacsListener>
public synchronized void addEmacsListener (EmacsListener l) {
listeners.add (l);
}
public synchronized void removeEmacsListener (EmacsListener l) {
listeners.remove (l);
}
private synchronized void callback (String type, Object[] args) {
int idx = type.indexOf ('=');
if (idx == -1) {
System.err.println ("Bad callback string: " + type);
return;
}
String realType;
int seq;
try {
realType = type.substring (0, idx);
seq = Integer.parseInt (type.substring (idx + 1));
} catch (NumberFormatException nfe) {
System.err.println ("Bad callback string: " + type);
return;
}
boolean serial = false;
for (int i = 0; i < SERIAL_EVENTS.length; i++) {
if (realType.indexOf (SERIAL_EVENTS[i]) != -1) {
serial = true;
break;
}
}
boolean oos = serial && seq < serialSeqNum;
if (oos && DEBUG) System.err.println("OOS because: seq=" + seq + " serialSeqNum=" + serialSeqNum);
EmacsEvent ev = new EmacsEvent (this, realType, args, oos);
if (DEBUG) System.err.println ("Connection.callback: " + ev);
Iterator it = listeners.iterator ();
while (it.hasNext ())
((EmacsListener) it.next ()).callback (ev);
}
private static int seqNum = 1;
private int thisSeqNum = 0;
private int serialSeqNum = 0;
// Please only call within a synch (this) block:
private int getNextSeqNum () {
synchronized (Connection.class) {
return thisSeqNum = seqNum++;
}
}
public Object[] function (final String type, Object[] args) throws EmacsException {
final int mySeq;
Integer mySeqI;
Object hold;
synchronized (this) {
mySeq = getNextSeqNum ();
if (DEBUG) {
System.err.print("Calling Connection.function(" + type + "/" + mySeq);
for (int i=0; i<args.length; i++) System.err.print("," + args[i]);
System.err.println(")");
}
if (Thread.currentThread ().getName ().startsWith (THREAD_MAGIC))
throw new IllegalStateException ("Attempt to call a function within dynamic scope of connection server!");
hold = new Object () {
public String toString () { return "Connection-holding[" + type + "#" + mySeq + "]"; }
};
mySeqI = new Integer (mySeq);
//if (DEBUG) System.err.println ("holds.put for wait object " + hold);
holds.put (mySeqI, hold);
callWithSeq (type + '/' + mySeq, args, true);
}
Object result;
int failcount = 0;
while ((result = holds.get (mySeqI)) == hold) {
if (failcount > 0) System.err.println("failed once...");
if (failcount++ == 3) throw new EmacsException ("TIMEOUT (30 sec)!");
try {
synchronized (hold) {
hold.wait (10000L);
}
} catch (InterruptedException ign) {
ign.printStackTrace ();
}
}
//if (DEBUG) System.err.println ("holds.remove for whatever " + mySeqI);
holds.remove (mySeqI);
Object[] realResult = (Object[]) result;
if (DEBUG) {
System.err.print ("Returning from Connection.function #" + mySeq + " with [");
for (int i=0; i<realResult.length; i++) {
if (i > 0) System.err.print(",");
System.err.print(realResult[i]);
}
System.err.println("]");
}
if (realResult.length > 0 && realResult[0] == ERROR_RETURN) {
if (realResult.length == 2 && realResult[1] instanceof String)
throw new EmacsException ((String) realResult[1]);
else
throw new IllegalArgumentException ("Bad format for error indication!");
} else {
return realResult;
}
}
public synchronized void call (String type, Object[] args) throws EmacsException {
callWithSeq (type + "!" + getNextSeqNum (), args, false);
}
// Please only call within a synch (this) block:
private void callWithSeq (String type, Object[] args, boolean isFunction) throws EmacsException {
// Check whether it is serial or not:
boolean serial = false;
String[] types = isFunction ? SERIAL_FUNCTIONS : SERIAL_COMMANDS;
for (int i = 0; i < types.length; i++) {
// XXX this is very ugly...should find these a nicer way
if (type.indexOf (types[i]) != -1) {
serial = true;
break;
}
}
if (DEBUG) {
System.err.print("Connection.call(" + type);
for (int i = 0; i < args.length; i++) System.err.print ("," + args[i]);
System.err.println(",serial=" + serial + ")");
}
if (serial) serialSeqNum = thisSeqNum;
try {
wr.write (type);
for (int i = 0; i < args.length; i++) {
wr.write (' ');
Object o = args[i];
if (o instanceof Integer) {
if (((Integer) o).intValue () < 0)
throw new IllegalArgumentException ("Cannot send negative numbers: " + o);
wr.write (o.toString ());
} else if (o instanceof String) {
String s = (String) o;
// XXX remove ref to Utilities class, unnecessary
s = Utilities.replaceString (s, "\\", "\\\\");
s = Utilities.replaceString (s, "\n", "\\n");
s = Utilities.replaceString (s, "\"", "\\\"");
s = Utilities.replaceString (s, "\r", "\\r");
wr.write ('"');
wr.write (s);
wr.write ('"');
} else if (o instanceof Boolean) {
wr.write (((Boolean) o).booleanValue () ? 'T' : 'F');
} else {
throw new IllegalArgumentException (o.toString ());
}
}
wr.write ('\n');
wr.flush ();
} catch (IOException e) {
e.printStackTrace ();
throw new EmacsException ((Exception) e.fillInStackTrace ());
}
}
public String toString () {
return "Connection[" + info + "]@" + thisSeqNum;
}
}